使用 CallerArgumentExpression 簡化參數檢核
TLDR
- 使用
[CallerArgumentExpression]屬性,可以在呼叫方法時自動擷取傳入參數的變數名稱,無需手動傳入字串。 - 相比使用
Expression<Func<T>>的方式,此方法效能更好且程式碼更簡潔。 - 配合
[NotNull]或[DoesNotReturn]等屬性,能有效支援 .NET 的 Nullable reference type 靜態分析,解決編譯器誤報警告的問題。 - 建議優先使用 .NET 6/7 內建的
ArgumentNullException.ThrowIfNull等方法,若需自訂檢核邏輯,可參考此模式實作。
使用 Expression 進行參數檢核的問題
在開發套件時,為了簡化參數檢核並統一錯誤訊息,開發者常會建立 ExceptionUtils 工具類。傳統做法是利用 Expression 來擷取變數名稱,避免重複輸入參數名稱字串:
csharp
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
if (expression.Body is not MemberExpression expressionBody) {
throw new ArgumentException("Expression 表達式錯誤。", nameof(expression));
}
return expressionBody.Member.Name;
}
}什麼情況下會遇到這個問題:當專案啟用 Nullable reference type 檢查時,上述使用 Expression 的方式無法讓編譯器辨識檢查後的變數不為 null,導致編譯器持續發出警告。此外,使用 Expression 需要編譯表達式,會帶來額外的效能開銷。
引入 CallerArgumentExpression 的解決方案
從 .NET 6 開始,官方引入了 [CallerArgumentExpression] 屬性。當方法參數標記此屬性時,編譯器會自動將呼叫端傳入該參數的「變數名稱」作為字串傳入。
實作範例
透過此屬性,我們可以重構檢核邏輯,使其既簡潔又能正確支援編譯器檢查:
csharp
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
public static class ExceptionUtils {
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
if (argument is null) {
throw new ArgumentNullException(paramName);
}
}
}驗證結果
測試程式碼如下:
csharp
string? str = "";
TestCallerArgumentExpression(str); // 未傳入 paramName
TestCallerArgumentExpression(str, "str2"); // 有傳入 paramName
void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
Console.WriteLine("paramName:" + paramName);
}執行結果:
text
paramName: str
paramName: str2什麼情況下會遇到這個問題:當需要自訂檢核邏輯(例如檢查特定格式或範圍),且希望在拋出例外時自動帶入變數名稱,同時保持編譯器對 Nullable reference type 的正確判斷時。
結論與建議
- 優先使用內建方法:.NET 6 與 .NET 7 已經內建了
ArgumentNullException.ThrowIfNull、ArgumentException.ThrowIfNullOrEmpty等方法,這些方法內部均已實作[CallerArgumentExpression],應優先採用。 - 效能優勢:使用
[CallerArgumentExpression]取代Expression<Func<T>>,可以避免執行時期的Compile()與Invoke()操作,顯著提升效能。 - 編譯器支援:透過搭配
[NotNull]或[DoesNotReturn]等屬性,可以讓編譯器正確識別程式碼的執行路徑,減少不必要的 Nullable 警告。
異動歷程
- 初版文件建立。